Skip to content

[#12331] Fix clean/site lifecycle bindings executing after pom.xml goals#12334

Merged
gnodet merged 2 commits into
masterfrom
lifecycle-bindings
Jun 23, 2026
Merged

[#12331] Fix clean/site lifecycle bindings executing after pom.xml goals#12334
gnodet merged 2 commits into
masterfrom
lifecycle-bindings

Conversation

@hboutemy

@hboutemy hboutemy commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

Fix goal bindings in pom.xml being executed before lifecycle bindings for
the clean and site lifecycles (regression vs Maven 3, fixes #12331).

Root cause

Lifecycles.plugin() creates executions for built-in clean and site
lifecycle bindings (e.g. default-clean) without setting a priority, so they
defaulted to 0 — the same as user-defined executions from pom.xml.

DefaultLifecycleMappingDelegate sorts executions within a phase by priority.
When both a lifecycle binding and a pom.xml execution share priority 0,
ordering falls back to plugin iteration order. Because LifecycleBindingsMerger
inserts pom.xml plugins before lifecycle-only plugins, pom.xml executions ended
up running first.

For the default lifecycle this was not a problem: packaging bindings (e.g. for
jar) are built by DefaultPackagingRegistry with priority(i - mojos.size())
— always negative — so they already sort before user executions.

Fix

Add .priority(-1) to the PluginExecution built in Lifecycles.plugin(),
making clean and site lifecycle bindings consistent with how default
lifecycle packaging bindings work.

Test plan

  • LifecycleExecutorTest, DefaultLifecyclesTest, BuildPlanCreatorTest
    all pass (561 tests, 0 failures in maven-core)
  • Manual reproduction from goal bindings in pom.xml executed BEFORE lifecycle ones with Maven 4 #12331: mvn clean with an antrun execution
    bound to the clean phase now runs default-clean (maven-clean-plugin)
    before the pom.xml antrun execution, matching Maven 3 behavior

@hboutemy hboutemy added the bug Something isn't working label Jun 20, 2026
@hboutemy hboutemy added this to the 4.1.0 milestone Jun 20, 2026
@hboutemy hboutemy requested a review from gnodet June 20, 2026 20:05
@gnodet gnodet added the mvn4 label Jun 22, 2026

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully automatic review from Claude Code

Review of PR #12334

Root Cause Analysis — Verified

I traced the full execution path. The PR description is accurate.

DefaultLifecycleRegistry.CleanLifecycle (line 429) and SiteLifecycle (line 547) call Lifecycles.plugin(...) to create lifecycle bindings. Before this PR, the PluginExecution was built without setting priority, defaulting to 0 — the same as user-defined executions from pom.xml.

DefaultLifecycleMappingDelegate (line 99) sorts executions within a phase using a TreeMap<PhaseId, ...> with PhaseComparator, which sorts by Integer.compare(p1.priority(), p2.priority()) (line 65) — lower priority number first. When priorities are tied (both 0), ordering falls to plugin iteration order.

LifecycleBindingsMerger.mergePluginContainer_Plugins inserts pom.xml plugins into the LinkedHashMap first (as target), lifecycle plugins second (as source). So at equal priority, pom.xml executions come first — causing the regression.

For comparison, DefaultPackagingRegistry (line 155) sets priority(i - mojos.size()) for default lifecycle packaging bindings, always producing negative values. This is why the default lifecycle was unaffected.

The Fix — Correct

Adding .priority(-1) makes clean/site bindings sort before user executions (priority 0) in both execution paths:

  1. Sequential path: DefaultLifecycleMappingDelegatePhaseComparatorInteger.compare(-1, 0) → lifecycle binding first
  2. Concurrent path: BuildPlanExecutor (line 600) → BuildStep.addMojo(mojo, priority)TreeMap<Integer, ...> key ordering → lifecycle binding first

The LifecycleBindingsMerger.mergePluginExecution_Priority also handles this correctly — it keeps min(target, source), so the -1 from lifecycle bindings is preserved during model merging. The compat layer (DefaultLifecycleBindingsInjector, line 154) uses Math.min() — same correct behavior.

Observations

The value -1 is consistent. DefaultPackagingRegistry uses i - mojos.size() which for a single mojo (i=0, size=1) produces exactly -1. Since Lifecycles.plugin() always creates a single execution, hardcoding -1 is equivalent.

No test for the fixed behavior. Existing tests pass and manual reproduction confirms the fix, but there's no new test that asserts lifecycle bindings execute before pom.xml executions in the clean/site lifecycle. This is the kind of regression that could easily reoccur if someone refactors lifecycle binding creation. A unit test or integration test verifying the execution order (like the manual reproducer from #12331) would guard against that.

The PR description is excellent — clear root cause, comparison with the working default lifecycle path, and a concise fix section.

Verify that lifecycle-default bindings (default-clean) run before
pom.xml-defined executions bound to the same phase, matching
Maven 3 behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully automatic review from Claude Code

Fix is correct, minimal, and now has test coverage. Approving.

The .priority(-1) value is consistent with DefaultPackagingRegistry (which produces -1 for single-mojo bindings via i - mojos.size()), both execution paths (sequential via PhaseComparator and concurrent via BuildStep.mojos TreeMap) sort correctly, and the merger preserves the lower priority during model merging.

@gnodet gnodet changed the title increase lifecycle bindings priority against pom ones [#12331] Fix clean/site lifecycle bindings executing after pom.xml goals Jun 23, 2026
@gnodet gnodet merged commit 6cbe5e2 into master Jun 23, 2026
42 of 43 checks passed
@gnodet gnodet deleted the lifecycle-bindings branch June 23, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mvn4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

goal bindings in pom.xml executed BEFORE lifecycle ones with Maven 4

2 participants